// TODO: CODE REVIEW & CLEANUP!
//
// TouchScrollViewerWithRows.cs
//
// Implements TouchScrollViewerWithRows and related types.
//

using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using HomeUX.UiUtilities;
using HomeUX.Utilities;

namespace HomeUX.Controls
{

/// <summary>
/// A touch-friendly control that vertically scrolls a series of rows.
/// </summary>
/// 
/// <remarks>
/// <para>
/// Unlike <r>TouchScrollViewer</r>, which treats its extent (i.e. the content it scrolls) as a
/// single uniform object, <r>TouchScrollViewerWithRows</r> has knowledge of a series of "rows"
/// (arbitrary elements) within the extent.  As a result, when the user scrolls, the viewport
/// (i.e. the part of the extent that's visible at any given time) can align to the top or
/// bottom of the rows as needed.
/// </para>
/// <para>
/// Scrolling can be accomplished in several ways: on-screen scroll buttons, a slider (which works
/// like a scrollbar), keyboard keys (see below), and dragging the extent directly.
/// </para>
/// <para>
/// Note that this control has no concept of pseudofocus (i.e. a focused item or row within
/// the extent).  Navigating using the keyboard arrow and page up/down keys does the same thing
/// as the on-screen scroll buttons.
/// </para>
/// <para>
/// Note: Unless you override <r>Rows</r>, <n>Content</n> must conform to some specific rules --
/// see <r>Rows</r> for more information.
/// </para>
/// </remarks>
///
public class TouchScrollViewerWithRows : TouchScrollViewer
{
    /// <summary>
    /// TODO
    /// </summary>
    ///
    public static readonly DependencyProperty ScrollPeekAmountProperty =
        DependencyProperty.Register("ScrollPeekAmount", typeof(double), typeof(TouchScrollViewerWithRows),
        null);

    /// <summary>
    /// TODO
    /// </summary>
    ///
    public double ScrollPeekAmount
    {
        get
        {
            return (double)GetValue(ScrollPeekAmountProperty);
        }
        set
        {
            SetValue(ScrollPeekAmountProperty, value);
        }
    }

    /// <summary>
    /// TODO
    /// </summary>
    ///
    public TouchScrollViewerWithRows()
    {
        DefaultStyleKey = typeof(TouchScrollViewer);

        // initialize properties (can't do it in Generic.xaml because then we'd need to duplicate
        // the template of TouchScrollViewer, since Silverlight doesn't support Style.BasedOn)
        ScrollPeekAmount = 10;
    }

    /// <summary>
    /// Gets the collection of elements that represent rows.
    /// </summary>
    /// 
    /// <remarks>
    /// <para>
    /// The default implementation assumes that <n>Content</n> consists of an outer <n>Panel</n>
	/// (e.g. a <n>Grid</n>) whose first child is another <n>Panel</n> (e.g. a <n>StackPanel</n>)
	/// that contains only controls that represent rows.
    /// </para>
    /// <para>
    /// Rows must not overlap (in x-y coordinate space), and the y-coordinates of rows must
    /// increase as the index into the <n>UIElementCollection</n> increases.  Row elements must
    /// derive from <n>FrameworkElement</n>.
    /// </para>
    /// <para>
    /// If you override <r>Rows</r>, you should also override <r>GetIndexOfRow</r> and
    /// <r>ContainsRow</r>.
    /// </para>
    /// <para>
    /// The indexer on the retrieved <n>IList</n> is assumed to have an O(1) implementation.
    /// </para>
    /// </remarks>
    ///
    public virtual IList<FrameworkElement> Rows
    {
        get
        {
            UIElementCollection rows = RowsDefaultImplementation;
            return new SimpleList<FrameworkElement>(rows.Count,
				index => (FrameworkElement)rows[index]);
        }
    }

    /// <summary>
    /// Gets the index of a given row within the collection of rows.  This operation may be
    /// O(<i>n</i>) in the worst case, where <i>n</i> is the number of rows.
    /// </summary>
    ///
    /// <param name="row">The row.</param>
    ///
    /// <returns>
    /// The zero-based index of the row.
    /// </returns>
    ///
    /// <remarks>
    /// You should also override <r>GetIndexOfRow</r> and <r>ContainsRow</r> if you override <r>Rows</r>. 
    /// </remarks>
    ///
    protected virtual int GetIndexOfRow(FrameworkElement row)
    {
        UIElementCollection rows = RowsDefaultImplementation;
        return rows.IndexOf(row);
    }

    /// <summary>
    /// Returns <n>true</n> if a given <n>FrameworkElement</n> is a row in this
    /// <r>TouchScrollViewerWithRows</r>, <n>false</n> if not.  This operation is O(1).
    /// </summary>
    ///
    /// <param name="row">The row to check.</param>
    ///
    protected virtual bool ContainsRow(FrameworkElement row)
    {
        return row.Parent == RowContainerDefaultImplementation;
    }

    /// <summary>
    /// Returns the index within <r>Rows</r> of the bottommost row for which the row itself or
    /// <r>ScrollPeekAmount</r> above the row is obscured from view above at the top of the
    /// viewport.  Returns -1 if no such row is found.
    /// </summary>
    ///
    public int FindRowAboveViewport()
    {
        // set <rows> to the collection of elements comprising the rows
        IList<FrameworkElement> rows = Rows;

        // set <rowIndex> to the index within <rows> of the last row whose top edge minus
        // ScrollPeekAmount is above the top edge of the viewport; the 0.5 is so that if the top
        // edge of a row minus ScrollPeekAmount exactly aligns with the top of the viewport the
        // row should not be considered above the viewport
        double y = ScrollViewer.VerticalOffset - 0.5;
        int rowIndex = Util.BinarySearch(rows.Count, y, Util.Compare,
            delegate(int i)
            {
                FrameworkElement row = rows[i];
                Point pt = UiUtil.GetElementTopLeft(row, Extent);
                return pt.Y - ScrollPeekAmount;
            });

        // return -1 if no row has its top edge - ScrollPeekAmount above the viewport; for example,
        // if there's a large gap or margin at the top of the extent (before the first row)
        if (rowIndex < 0)
            return -1;

        // return the index of the row we found
        return rowIndex;
    }

    /// <summary>
    /// Returns the index within <r>Rows</r> of the topmost row for which the row itself or
    /// <r>ScrollPeekAmount</r> below the row is obscured from view below at the bottom of the
    /// viewport.  Returns -1 if no such row is found.
    /// </summary>
    ///
    public int FindRowBelowViewport()
    {
        int rowIndex = FindRowAtBottomOfViewport();
        if (rowIndex < 0)
            return 0;
        else
        {
            IList<FrameworkElement> rows = Rows;
            if (rowIndex >= rows.Count - 1)
                return -1;
            else
                return rowIndex + 1;
        }
    }

    /// <summary>
    /// Returns the index withing <r>Rows</r> of the topmost row for which the top edge of the row
    /// and ScrollPeekAcount--?? TODO above the row are visible in the viewport.  Returns -1 if no
    /// such row is found.
    /// </summary>
    ///
    public int FindRowAtTopOfViewport()
    {
        // set <rows> to the collection of elements comprising the rows
        IList<FrameworkElement> rows = Rows;

        // set <rowIndex> to the index within <Rows> of the bottommost row for which the row
        // itself or ScrollPeekAmount above the row is obscured from view above at the top of the
        // viewport; if no such row is found, set <rowIndex> to -1
        int rowIndex = FindRowAboveViewport();

        // advance <rowIndex> to the next row (or the first row, if none was found), which is the
        // candidate to be the row at the top of the viewport
        if (rowIndex < 0)
            rowIndex = 0;
        else
            rowIndex++;

        // see if the row exists
        if (rowIndex >= rows.Count)
            return -1;

        // check if the <ScrollPeekAmount> above the top of the row is visible
        var row = rows[rowIndex];
        Point pt = UiUtil.GetElementTopLeft(row, Extent);
        double y = ScrollViewer.VerticalOffset;
        return (y <= pt.Y - ScrollPeekAmount) ? rowIndex : -1;
    }

    /// <summary>
    /// Returns the index withing <r>Rows</r> of the bottommost row for which the bottom edge of
    /// the row and ScrollPeekAcount--?? TODO beneath the row are visible in the viewport.  Returns
    /// -1 if no such row is found.
    /// </summary>
    ///
    public int FindRowAtBottomOfViewport()
    {
        // set <rows> to the collection of elements comprising the rows
        IList<FrameworkElement> rows = Rows;

        // set <rowIndex> to the index within <rows> of the last row whose bottom edge plus
        // ScrollPeekAmount is less than the bottom edge of the viewport
        double y = ScrollViewer.VerticalOffset + ScrollViewer.ViewportHeight;
        int rowIndex = Util.BinarySearch(rows.Count, y, Util.Compare,
            delegate(int i)
            {
                FrameworkElement row = rows[i];
                Point pt = UiUtil.GetElementTopLeft(row, Extent);
                return pt.Y + row.ActualHeight + ScrollPeekAmount;
            });

        // return -1 if no row has its bottom edge + ScrollPeekAmount visible within the viewport;
        // for example, if there's a large gap or margin at the top of the extent (before the
        // first row), or if the first row is very tall
        if (rowIndex < 0)
            return -1;

        // return the index of the row we found
        return rowIndex;
    }

    public bool ScrollIntoView(FrameworkElement row, ScrollAlignment alignment)
    {
        // nothing to do if no scrolling is desired
        if (alignment == ScrollAlignment.None)
            return true;

        // if it's too early in the layout process to perform scrolling, return false
        if (row.ActualHeight == 0)
            return false;

        // set <rect> to be the bounding rectangle of <row>, relative to the top-left corner of the
		// extent
        Rect rect = UiUtil.GetElementBoundingRect(row, Extent);

        // if <rect> is at least partially above the viewport (or ScrollAlignment.Top is specified),
        // raise the viewport so that <rect> is visible, with an
        // extra <peek> vertical units above it so it's easy to see if there are more items above it
        double dy = ScrollViewer.VerticalOffset - rect.Top;
        if ((dy > 0) || (alignment == ScrollAlignment.Top))
        {
            ScrollToOffset(ScrollViewer.VerticalOffset - dy - ScrollPeekAmount, true, true);
            return true;
        }

        // if <rect> is at least partially below the viewport (or ScrollAlignment.Bottom is
        // specified), lower the viewport so that <rect> is visible, with an
        // extra <peek> vertical units below it so it's easy to see if there are more items above it
        dy = rect.Bottom - (ScrollViewer.VerticalOffset + ScrollViewer.ViewportHeight);
        if ((dy > 0) || (alignment == ScrollAlignment.Bottom))
        {
            ScrollToOffset(ScrollViewer.VerticalOffset + dy + ScrollPeekAmount, true, true);
            return true;
        }

        return true;
    }

    /// <summary>
    /// Performs the action of a specified keyboard navigation button (i.e. an arrow key, Page Up,
    /// Page Down, Home, End).
    /// </summary>
    ///
    /// <param name="key">The key.</param>
    ///     
    /// <returns>
    /// <n>true</n> if the key was handled, <n>false</n> if not.
    /// </returns>
    ///
    protected override bool PerformKeyboardNavigation(Key key)
    {
        // this control has no concept of pseudofocus, so e.g. the up-arrow key on the keyboard is
        // simply mapped to the same function as the on-screen line-up scroll button
        return PerformScrollButtonNavigation(key);
    }

    /// <summary>
    /// Performs the action of a specified on-screen scroll button (line up or down, page up or
    /// down), as well as Home and End functions.
    /// </summary>
    ///
    /// <param name="key">The keyboard key which most closely matches the on-screen scroll button,
    ///     i.e. Up, Down, PageUp, PageDown.  Home and End may also be used.</param>
    ///     
    /// <returns>
    /// <n>true</n> if the key was handled, <n>false</n> if not.
    /// </returns>
    ///
    protected override bool PerformScrollButtonNavigation(Key key)
    {
        switch (key)
        {
        case Key.Up:
            LineUp();
            return true;
        case Key.Down:
            LineDown();
            return true;
        case Key.PageUp:
            PageUp();
            return true;
        case Key.PageDown:
            PageDown();
            return true;
        case Key.Home:
            ScrollToOffset(0, true, true);
            return true;
        case Key.End:
            ScrollToOffset(ScrollViewer.ExtentHeight - ScrollViewer.ViewportHeight, true, true);
            return true;
        }

        return false;
    }

    /// <summary>
    /// Scrolls up one line.  Does nothing if such scrolling is impossible.
    /// </summary>
    ///
    protected void LineUp()
    {
        // set <rowIndex> to the index within <rows> of the last row that's at least partly
        // obscured from view above the viewport; if no such row is found, scroll to the top
        int rowIndex = FindRowAboveViewport();
        if (rowIndex < 0)
        {
            ScrollToOffset(0, true, true);
            return;
        }

        // scroll the row specified by <rowIndex> so that it's aligned with the top of the
        // viewport
        ScrollIntoView(Rows[rowIndex], ScrollAlignment.Top);
    }

    /// <summary>
    /// Scrolls down one line.  Does nothing if such scrolling is impossible.
    /// </summary>
    ///
    protected void LineDown()
    {
        // set <rowIndex> to the index within <rows> of the first row that's at least partly
        // obscured from view beneath the viewport; if no such row is found, scroll to the bottom
        int rowIndex = FindRowBelowViewport();
        if (rowIndex < 0)
        {
            ScrollToOffset(ScrollViewer.ExtentHeight + ScrollViewer.ViewportHeight, true, true);
            return;
        }

        // scroll the row specified by <rowIndex> so that it's aligned with the bottom of the
        // viewport
        ScrollIntoView(Rows[rowIndex], ScrollAlignment.Bottom);
    }

    /// <summary>
    /// Scrolls up one page.  Does nothing if such scrolling is impossible.
    /// </summary>
    protected void PageUp()
    {
        // set <rows> to the collection of elements comprising the rows
        IList<FrameworkElement> rows = Rows;

        // set <rowIndex> to the index within <Rows> of the first row that's fully visible in the
        // the viewport (including ScrollPeekAmount above it); if no such row is found, scroll to
        // the top
        int rowIndex = FindRowAtTopOfViewport();
        if (rowIndex < 0)
        {
            ScrollToOffset(0, true, true);
            return;
        }

        // set <row> to the the row specified by <rowIndex>
        var row = rows[rowIndex];

        // we want to scroll up so that <y>, defined here as ScrollPeekAmount below the bottom of
        // <row>, is below the viewport; advance <row> backward to the row that we need to scroll
        // into view (including ScrollPeekAmount at the top) to accomplish that
        double scrollPeekAmount = ScrollPeekAmount;
        double y = UiUtil.GetElementBoundingRect(row, Extent).Bottom + scrollPeekAmount;
        while (true)
        {
            // advance <rowIndex> and <row> backward to the previous row; if we run out of rows,
            // scroll to the top and return
            if (--rowIndex < 0)
            {
                ScrollToOffset(0, true, true);
                return;
            }
            row = rows[rowIndex];

            // if we've advanced far enough, stop
            Point pt = UiUtil.GetElementTopLeft(row, Extent);
            if (y - pt.Y - scrollPeekAmount > ScrollViewer.ViewportHeight)
                break;
        }

        // scroll <row> into view, aligning its top (minus ScrollPeekAmount) with the top of
        // the viewport
        ScrollIntoView(row, ScrollAlignment.Top);
    }

    /// <summary>
    /// Scrolls down one page.  Does nothing if such scrolling is impossible.
    /// </summary>
    protected void PageDown()
    {
        // set <rows> to the collection of elements comprising the rows
        IList<FrameworkElement> rows = Rows;

        // set <rowIndex> to the index within <Rows> of the last row that's fully visible in the
        // viewport (including ScrollPeekAmount below it); if no such row is found, scroll to the
        // bottom and return
        int rowIndex = FindRowAtBottomOfViewport();
        if (rowIndex < 0)
        {
            ScrollToOffset(ScrollViewer.ExtentHeight + ScrollViewer.ViewportHeight, true, true);
            return;
        }

        // set <row> to the row specified by <rowIndex>
        FrameworkElement row = rows[rowIndex];

        // we want to scroll down so that <y>, defined here as ScrollPeekAmount above the top of
        // <row>, is above the viewport; advance <row> to the row that we need to scroll into
        // view (including ScrollPeekAmount at the bottom) to accomplish that
        double scrollPeekAmount = ScrollPeekAmount;
        double y = UiUtil.GetElementTopLeft(row, Extent).Y - scrollPeekAmount;
        while (true)
        {
            // advance <rowIndex> and <row> to the next row; if we run out of rows, scroll to the
            // bottom and return
            if (++rowIndex >= rows.Count)
            {
                ScrollToOffset(ScrollViewer.ExtentHeight + ScrollViewer.ViewportHeight, true, true);
                return;
            }
            row = rows[rowIndex];

            // if we've advanced far enough, stop
            Rect rect = UiUtil.GetElementBoundingRect(row, Extent);
            if (rect.Bottom + scrollPeekAmount - y > ScrollViewer.ViewportHeight)
                break;
        }

        // scroll <row> into view, aligning its bottom (plus ScrollPeekAmount) with the bottom of
        // the viewport
        ScrollIntoView(row, ScrollAlignment.Bottom);
    }

    /// <summary>
    /// Returns the <n>UIElementCollection</n> that comprises the collection of rows in the
    /// default implementation (see <r>Rows</r> for more information).
    /// </summary>
    ///
    UIElementCollection RowsDefaultImplementation
    {
        get
        {
            return RowContainerDefaultImplementation.Children;
        }
    }

    /// <summary>
    /// Returns the <n>Panel</n> that contains rows in the default implementation (see <r>Rows</r>
    /// for more information).
    /// </summary>
    ///
    Panel RowContainerDefaultImplementation
    {
        get
        {
            var outerPanel = (Panel)Extent;
            return (Panel)outerPanel.Children[0];
        }
    }
}

/// <summary>
/// Specifies how a specified item within a scrollable region should be aligned with the viewport
/// when a given scrolling operation completes.
/// </summary>
public enum ScrollAlignment
{
    /// <summary>
    /// No scrolling is performed.
    /// </summary>
    None,

    /// <summary>
    /// Scroll so that the specified item is aligned with the top of the viewport.
    /// </summary>
    Top,

    /// <summary>
    /// Scroll so that the specified item is aligned with the bottom of the viewport.
    /// </summary>
    Bottom,

    /// <summary>
    /// Scroll so that the specified item is aligned with the top of the viewpoert if it's above the viewport,
    /// the bottom of the viewport if it's below the viewport.  If the specified item is already in view, no scrolling
    /// is performed.
    /// </summary>
    IntoView
}

}
